package org.softee.extractor;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.namespace.QName;

public class QNameMatcherFactory {
    private static class QNameMatcherImpl implements QNameMatcher {
        /**
         * Matches "LocalName", "{}LocalName" and "{nsUri}LocalName"
         */
        private static final Pattern QNAME_PATTERN = Pattern.compile("(?:\\{([^}]*)\\})?(.+)");

        private final String localPart;
        private final String namespaceUri;

        private QNameMatcherImpl(String combinedName) {
            Matcher matcher = QNAME_PATTERN.matcher(combinedName);
            if (!matcher.matches()) {
                throw new IllegalStateException("Unable to match: '" + combinedName + "'"); // everything matches
            }
            namespaceUri = matcher.group(1); // null if no curly braces was found
            localPart = matcher.group(2);
        }

        private QNameMatcherImpl(QName qName) {
            this(qName.getNamespaceURI(), qName.getLocalPart());
        }

        private QNameMatcherImpl(String namespaceUri, String localPart) {
            this.namespaceUri = namespaceUri;
            this.localPart = localPart;
        }

        /**
         * Matches this name against a namespace and a local name.
         * If the {@link #namespace} is null, it will
         * match using {@link QName#getLocalPart()}, ignoring any namespace.
         * @param namespace The namespace of a QName to match against, or null to match any namespace
         * @param localPart the local part of a QName to match against
         * @return true if this Name matches
         */
        @Override
        public boolean matches(String namespace, String localPart) {
            if (this.namespaceUri == null) {
                return eq(this.localPart, localPart);
            } else {
                return eq(this.localPart, localPart) && eq(this.namespaceUri, namespace);
            }
        }

        private static boolean eq(Object o1, Object o2) {
            return (o1 == null) ? o2 == null : o1.equals(o2);
        }
    }

    private static QNameMatcher[] matchers(String... combinedNames) {
        QNameMatcherImpl[] names = new QNameMatcherImpl[combinedNames.length];
        for (int i = 0; i < combinedNames.length; i++) {
            names[i] = new QNameMatcherImpl(combinedNames[i]);
        }
        return names;
    }

    private static QNameMatcher[] matchers(QName... qNames) {
        QNameMatcherImpl[] names = new QNameMatcherImpl[qNames.length];
        for (int i = 0; i < qNames.length; i++) {
            names[i] = new QNameMatcherImpl(qNames[i]);
        }
        return names;
    }

    /**
     * @param matchers (0..n) name matchers
     * @return true if at least one of matchers matches, false otherwise
     */
    private static QNameMatcher compoundMatcher(final QNameMatcher... matchers) {
        return new QNameMatcher() {
            @Override
            public boolean matches(String namespaceUri, String localPart) {
                for (QNameMatcher matcher : matchers) {
                    if (matcher.matches(namespaceUri, localPart)) {
                        return true;
                    }
                }
                return false;
            }
        };
    }

    /**
     * @param combinedName a string containing a Qualified name with an optional namespace URI.
     * Syntax: {&lt;namespaceURI&gt;}&lt;localPart&gt;
     * @return a {@code QNameMatcher} with the following match profile:
     * <ul>
     * <li>If {@code combinedName} contains no namespaceURI part:<br/>
     * Matches ANY element where localPart matches {@code combinedName}, regardless of whether that element belongs to a namespace, or not
     * </li>
     * <li>If {@code combinedName} contains a namespaceURI part (which may be empty}:<br/>
     * Matches elements where both namespaceURI AND localPart matches those specified by {@code combinedName}
     * </li>
     * </ul>
     * Examples of valid {@code combinedName}s:<br>
     * <ol>
     * <li>"{http://softee.org}ChildElement"<br/>
     * Matches the XML element {@code <ChildElement xmlns="http://softee.org" />}, but NOT the namespaceless XML element {@code <ChildElement/>}
     * </li>
     * <li>"{}ChildElement"<br/>
     * Matches the namespaceless XML element {@code <ChildElement/>}, but NOT the XML element {@code <ChildElement xmlns="http://softee.org" />}
     * </li>
     * <li>"ChildElement"<br/>
     * Matches any XML element whose localName is "ChildElement", e.g. the namespaceless XML element {@code <ChildElement/>} as well as {@code <ChildElement xmlns="http://softee.org" />
     * </li>
     * </ol>
     */
    public static QNameMatcher combinedNameMatcher(final String combinedName) {
        return new QNameMatcherImpl(combinedName);
    }

    /**
     * @param combinedNames zero or more combinedNames.
     * @return a QName matcher that matches ANY of the {@code combinedNames}
     * @see #combinedNameMatcher(String)
     */
    public static QNameMatcher combinedNameMatcher(final String... combinedNames) {
        return compoundMatcher(matchers(combinedNames));
    }

    /**
     * @param qName a QName to match
     * @return a QNameMatcher that matches any element whose QName equals qName. If qName has a null
     * namespaceURI, it will match ANY element where the localPart of the element matches the localPart of qName,
     * regardless of whether the element belongs to a namespace, or not
     */
    public static QNameMatcher qNameMatcher(final QName qName) {
        return new QNameMatcherImpl(qName);
    }

    /**
     * @param qNameTargets
     * @return
     * @see #qNameMatcher(QName)
     */
    public static QNameMatcher qNameMatcher(final QName... qNameTargets) {
        return compoundMatcher(matchers(qNameTargets));
    }

    /**
     * @return a QName matcher that will successfully match ANY element
     */
    public static QNameMatcher matchAll() {
        return new QNameMatcher() {
            @Override
            public boolean matches(String uri, String localName) {
                return true;
            }
        };
    }
}
